當我們在開發產品的時候,使用者驗證是不能跳過的重要一環,選用好的供應商可以省去很多的維運的成本,讓你的晚上睡的更安穩。
因為我們起手式使用了 GCP,所以不免俗就要拿 GCP 的服務跟其他供應商去比較。所以下面就會針對 GCP Identity Platform 跟 Auth0 兩者中去選擇一個來作為我們的認證服務的供應商。
Auth0 是在社群上能見度很高的身分認證管理服務平台,提供各種語言的 SDK 與相關功能,可以達到一站式管理從會員認證、權限管理,甚至是可以做到複雜的 pipeline 整合。不只是支援各家身分認證供應商的整合,也支援很多最新型的認證方法:像是 passwordless 、 MFA 與 Single Sign On ......等等。
GCP Identity Platform 提供類似的功能,但是都主要針對認證的服務,其他的延伸功能沒有支援到 auth0 這麼全面。但是也支援有名的身分認證供應商,跟基本的 email 與 MFA 認證方式。
雖然 auth0 提供了近乎連新手都可以順利串接的教學文章,但是因為目前針對 Octane 的相容支援還在路上,所以這次的實作就會以 GCP Identity 為例。
但是如果有機會可以使用 auth0 ,基本上就是閉著眼睛選也要使用的好選擇,各式各樣與支援不同語言的 SDK 與文件,對開發者非常友善。
無論選擇哪個平台,將身份驗證外包給第三方的供應商都有一些好處:
composer require "kreait/firebase-php:^7.0"
<?php
namespace App\Guard;
use App\Models\User;
use Exception;
use Illuminate\Http\Request;
use Kreait\Firebase\JWT\Error\IdTokenVerificationFailed;
use Kreait\Firebase\JWT\IdTokenVerifier;
use Kreait\Firebase\Contract\Auth;
class FirebaseGuard
{
public function __construct(
protected IdTokenVerifier $verifier,
protected Auth $auth,
)
{}
/**
* Get User by request claims.
*
* @throws Exception
*/
public function user(Request $request): mixed
{
$token = $request->bearerToken();
if (empty($token)) {
return null;
}
try {
$firebaseToken = $this->verifier->verifyIdToken($token);
/* @var User $user */
$user = app(config('auth.providers.users.model'));
return $user
->setFirebaseAuthenticationToken($token)
->resolveByPayload($firebaseToken->payload());
} catch (Exception $e) {
if ($e instanceof IdTokenVerificationFailed) {
return null;
}
if (config('app.debug')) {
throw $e;
}
return null;
}
}
}
FirebaseAuthenticable
並且新增至 User modelFirebaseAuthenticable.php
<?php
namespace App\Models\Trait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Kreait\Firebase\Contract\Auth;
use Kreait\Firebase\Exception\AuthException;
use Kreait\Firebase\Exception\FirebaseException;
trait FirebaseAuthenticable
{
protected array $claims;
protected ?string $firebaseAuthenticationToken;
/**
* @throws FirebaseException
* @throws AuthException
*/
public function resolveByPayload(array $payload): object
{
$id = data_get($payload, 'sub');
return $this->updateOrCreateUser($id, $this->transformPayload($payload));
}
/**
* Update or create user.
*
* @throws AuthException
* @throws FirebaseException
*/
public function updateOrCreateUser(string $id, array $attributes): object
{
if ($user = self::query()->find($id)) {
$user->fill($attributes);
if ($user->isDirty()) {
$user->save();
}
return $user;
}
/* @var Auth $firebaseAuth*/
$firebaseAuth = app(Auth::class);
$userInfo = $firebaseAuth->getUser($id);
$user = $this->fill(array_merge($attributes, [
'name' => $userInfo->displayName,
'info' => Arr::first($userInfo->providerData),
]));
$user->id = $id;
$user->save();
return $user;
}
protected function transformPayload(array $payload): array
{
$attributes = [];
if (!is_null(data_get($payload, 'firebase.identities.name'))) {
$attributes['name'] = (string) data_get($payload, 'firebase.identities.name');
}
return $attributes;
}
public function setFirebaseAuthenticationToken(string $token): self
{
$this->firebaseAuthenticationToken = $token;
return $this;
}
public function getFirebaseAuthenticationToken(): string
{
return $this->firebaseAuthenticationToken;
}
public function getAuthIdentifierName(): string
{
return 'id';
}
public function getAuthIdentifier(): mixed
{
return $this->id;
}
public function getAuthPassword(): string
{
throw new \RuntimeException('No password support for Firebase Users');
}
public function getRememberToken(): string
{
throw new \RuntimeException('No remember token support for Firebase Users');
}
/**
* Set the token value for the "remember me" session.
*
* @param string $value
*
* @return void
*/
public function setRememberToken($value): void
{
throw new \RuntimeException('No remember token support for Firebase User');
}
public function getRememberTokenName(): string
{
throw new \RuntimeException('No remember token support for Firebase User');
}
}
User.php
<?php
// ...
class User extends Authenticatable
{
use HasFactory;
use Notifiable;
use FirebaseAuthenticable;
// ...
}
AuthServiceProvider.php
<?php
// ...
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('firebase', function ($request) {
return app(FirebaseGuard::class)->user($request);
});
}
public function register(): void
{
$this->app->singleton(IdTokenVerifier::class, function ($app) {
$projectId = config('firebase.project_id', env('GOOGLE_CLOUD_PROJECT'));
if (empty($projectId)) {
throw new \RuntimeException('Missing GOOGLE_CLOUD_PROJECT env variable.');
}
return IdTokenVerifier::createWithProjectId($projectId);
});
}
config/auth.php
的 設定<?php
return [
// ...
'guards' => [
'api' => [
'driver' => 'firebase',
'provider' => 'users',
'hash' => false,
],
],
// ...
]